{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 3\n",
    "\n",
    "### <center> Автор материала: Безрученко Павел"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Индивидуальный проект по анализу данных </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**План исследования**\n",
    " - Описание набора данных и признаков\n",
    " - Первичный анализ признаков\n",
    " - Первичный визуальный анализ признаков\n",
    " - Закономерности, \"инсайты\", особенности данных\n",
    " - Предобработка данных\n",
    " - Кросс-валидация, подбор параметров\n",
    " - Построение кривых валидации и обучения \n",
    " - Прогноз для тестовой или отложенной выборки\n",
    " - Оценка модели с описанием выбранной метрики\n",
    " - Выводы\n",
    " \n",
    " Более детальное описание [тут](https://goo.gl/cJbw7V)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ссылка на данные: https://www.kaggle.com/uciml/breast-cancer-wisconsin-data/data\n",
    "\n",
    "Описание признаков: https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.names"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "\n",
    "from tqdm import tqdm\n",
    "from sklearn.model_selection import train_test_split, cross_val_score, learning_curve, StratifiedKFold, GridSearchCV\n",
    "from sklearn.metrics import accuracy_score, precision_score\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.manifold import TSNE\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "pd.set_option('display.height', 1000)\n",
    "pd.set_option('display.max_rows', 500)\n",
    "pd.set_option('display.max_columns', 500)\n",
    "pd.set_option('display.width', 1000)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv('data/data.csv', sep=',')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Признаки были получены из оцифрованных изображений (FNA) молочной железы. Они описывают\n",
    "характеристики ячеек клеток, присутствующих в изображении.\n",
    "\n",
    "Наша задача бинарно классифицировать пациентов по типу опухоли - доброкачественная или злокачественная.\n",
    "Целевой признак \"diagnosis\", значения которого (\"M\" и \"B\") соответсвуют диагнозу (\"Malignant\" или \"Benign\").\n",
    "\n",
    "Количество сэмплов: 569\n",
    "\n",
    "Количество фич: 32 (ID, диагноз и 30 переменных в формате real, обозначающих характеристики опухоли)\n",
    "\n",
    "В данных пропусков нет. \n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для каждой опухоли вычисляются десять вещественных признаков:\n",
    "\n",
    "+ **радиус**\n",
    "+ **текстура** (стандартное отклонение значений шкалы серого)\n",
    "+ **периметр**\n",
    "+ **площадь**\n",
    "+ **гладкость**\n",
    "+ **компактность** (периметр ^ 2 / площадь - 1,0)\n",
    "+ **вогнутость**\n",
    "+ **вогнутые точки** (количество вогнутых участков контура)\n",
    "+ **симметрия**\n",
    "+ **фрактальная размерность**\n",
    "    \n",
    "Mean, SE (standard error) и Worst этих признаков были рассчитаны для каждого изображения,\n",
    "в результате чего мы имеем 30 фич. \n",
    "\n",
    "Например, поле 3 представляет собой средний радиус, поле\n",
    "13 - Радиус SE, поле 23 является Worst Радиусом."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 2. Первичный анализ признаков"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на распределение целевого признака"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Malignant:', len(df[df['diagnosis'] == 'M']))\n",
    "print('Benign:', len(df[df['diagnosis'] == 'B']))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим датасет на наличие пропущенных значений"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.isnull().sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отлично, все значения присутствуют, кроме полностью нулевого признака, который мы позже удалим"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Основная статистическая информация:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.describe()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.get_ftype_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В датасете только один категориальный признак, который является целевым, все остальные числа с плавающей точкой"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 3. Предобработка данных "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Представим переменную diagnosis в бинарном виде"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.diagnosis = df.diagnosis.replace({'M': 1, 'B': 0})\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Удалим столбец с пустыми значениями и идетификаторы пациентов"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop(columns=['Unnamed: 32', 'id'], inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 4. Первичный визуальный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Более наглядное распределение целевой переменной:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.countplot(x=df['diagnosis'], palette=\"Set3\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим наши данные на наличие выбросов и посмотрим на разницу значений между переменными:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=[30, 30])\n",
    "ax = sns.boxplot(data=df, palette=\"Set3\")\n",
    "ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha=\"right\")\n",
    "ax.set_xticklabels(ax.get_xticklabels(), size='xx-large')\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на средний радиус в контексте целевой переменной"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.boxplot(x='radius_mean', y=df['diagnosis'].replace({1: 'Malignant', 0: 'Benign'}), data=df, palette=\"Set3\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что в основном средний радиус опухоли значительно больше у людей со злокачественной опухолью.\n",
    "\n",
    "Нужно проверить, есть ли отличия по другим признакам.\n",
    "\n",
    "Построим pairplot для mean фич:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ax = sns.pairplot(data=df, hue='diagnosis', \n",
    "             vars=['radius_mean', 'texture_mean', 'perimeter_mean', 'area_mean',\\\n",
    "                   'smoothness_mean', 'compactness_mean', 'concavity_mean', \\\n",
    "                   'concave points_mean', 'symmetry_mean', 'fractal_dimension_mean'])\n",
    "plt.figure(figsize=[30,30])\n",
    "plt.show();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для standart error:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ax = sns.pairplot(data=df, hue='diagnosis', \n",
    "             vars=['radius_se', 'texture_se', 'perimeter_se', 'area_se',\\\n",
    "                   'smoothness_se', 'compactness_se', 'concavity_se', \\\n",
    "                   'concave points_se', 'symmetry_se', 'fractal_dimension_se'])\n",
    "plt.figure(figsize=[30,30])\n",
    "plt.show();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для worst признаков:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ax = sns.pairplot(data=df, hue='diagnosis', \n",
    "             vars=['radius_worst', 'texture_worst', 'perimeter_worst', 'area_worst',\\\n",
    "                   'smoothness_worst', 'compactness_worst', 'concavity_worst', \\\n",
    "                   'concave points_worst', 'symmetry_worst', 'fractal_dimension_worst'])\n",
    "plt.figure(figsize=[30,30])\n",
    "plt.show();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Попробуем теперь уменьшить размерность наших данных и разбить их на кластеры"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scaler = StandardScaler()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "df_scaled = scaler.fit_transform(df.drop(columns=['diagnosis']))\n",
    "tsne = TSNE(random_state=22)\n",
    "tsne_representation_full = tsne.fit_transform(df_scaled)\n",
    "\n",
    "plt.scatter(tsne_representation_full[:, 0], tsne_representation_full[:, 1], \n",
    "            c=df['diagnosis'].map({0: 'blue', 1: 'orange'}));\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на матрицу корреляций:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=[30, 30])\n",
    "ax = sns.heatmap(df.corr(), fmt = \".1f\", cmap='YlGnBu', cbar = True, annot=True)\n",
    "ax.set_xticklabels(ax.get_xticklabels(), size='xx-large')\n",
    "ax.set_yticklabels(ax.get_yticklabels(), size='xx-large')\n",
    "sns.set(font_scale=1.4)\n",
    "plt.show();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 5. Закономерности, \"инсайты\", особенности данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Судя по boxplot'ам в данных присутствуют выбросы, пара признаков значительно отличаются от большинства по масштабу."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "У _mean и _worst признаков на большинстве графиков видно четкое разбиение по целевому признаку.\n",
    "Снижение размерности данных с помощью TSN-e также показало два четких кластера с несущественным количеством аномалий (которые могут быть выбросами или ошибками в данных). \n",
    "\n",
    "Подобный результат должен означать высокую точность предсказаний у модели.\n",
    "\n",
    "На матрице корреляций оказалось много значений, стремящихся к единице, в основном потому, что некоторые признаки вычисялются друг из друга или имеют прямую зависимость между собой (например: radius_mean и radius_worst)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 6. Кросс-валидация, подбор параметров"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве модели для классификации опухолей будем использовать случайный лес, т.к. данная модель нечувствительна к выбросам и может выдавать высокую точность без масштабирования и детальной настройки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = df.diagnosis\n",
    "X = df.drop(columns=['diagnosis'])\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=21)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rfc = RandomForestClassifier(n_jobs=-1, random_state=21)\n",
    "rfc_params = {'max_depth': range(2, 10),\n",
    "             'n_estimators': [2, 20, 100],\n",
    "             'criterion': ['entropy', 'gini'],\n",
    "             'max_features': ['sqrt', None], \n",
    "             'min_samples_split' : range(2, 6),\n",
    "             'max_leaf_nodes' : [100, None]}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Найдем наилучшее сочетание гипер-параметров в модели"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "rfc_search = GridSearchCV(rfc, param_grid=rfc_params, n_jobs=-1)\n",
    "rfc_search.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rfc_search.best_params_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 7. Построение кривых валидации и обучения "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,\n",
    "                        n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):\n",
    "    '''\n",
    "    http://scikit-learn.org/stable/auto_examples/model_selection/plot_learning_curve.html\n",
    "    '''\n",
    "    \n",
    "    plt.figure()\n",
    "    plt.title(title)\n",
    "    if ylim is not None:\n",
    "        plt.ylim(*ylim)\n",
    "    plt.xlabel(\"Training examples\")\n",
    "    plt.ylabel(\"Score\")\n",
    "    train_sizes, train_scores, test_scores = learning_curve(\n",
    "        estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes, scoring='roc_auc')\n",
    "    train_scores_mean = np.mean(train_scores, axis=1)\n",
    "    train_scores_std = np.std(train_scores, axis=1)\n",
    "    test_scores_mean = np.mean(test_scores, axis=1)\n",
    "    test_scores_std = np.std(test_scores, axis=1)\n",
    "    plt.grid()\n",
    "\n",
    "    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,\n",
    "                     train_scores_mean + train_scores_std, alpha=0.1,\n",
    "                     color=\"r\")\n",
    "    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,\n",
    "                     test_scores_mean + test_scores_std, alpha=0.1, color=\"g\")\n",
    "    plt.plot(train_sizes, train_scores_mean, 'o-', color=\"r\",\n",
    "             label=\"Training score\")\n",
    "    plt.plot(train_sizes, test_scores_mean, 'o-', color=\"g\",\n",
    "             label=\"Cross-validation score\")\n",
    "\n",
    "    plt.legend(loc=\"best\")\n",
    "    return plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rfc_best = RandomForestClassifier(max_depth=4, max_features='sqrt', max_leaf_nodes=100, \\\n",
    "                                  n_estimators=20, min_samples_split=2, random_state=21)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rfc_best.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(8, 6))\n",
    "plot_learning_curve(rfc_best, 'Random Forest', X_train, y_train, cv=3, n_jobs=-1);\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на важность признаков при обучении"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Feature ranking:')\n",
    "for f in range(X_train.shape[1]):\n",
    "    print('%d. feature %s (%f)' % (f + 1, X_train.columns[f],\n",
    "                                      rfc_best.feature_importances_[f]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Самыми важными признаками оказались radius_worst и perimeter_worst, забавно"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 8. Прогноз для тестовой или отложенной выборки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ради интереса попробуем обучить модель и сделать прогноз для двух выборок - для полной и для выборки только из двух признаков (radius_worst и perimeter_worst)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rfc_main_features = RandomForestClassifier(max_depth=4, max_features='sqrt', max_leaf_nodes=100, \\\n",
    "                                  n_estimators=20, min_samples_split=2, random_state=21)\n",
    "rfc_main_features.fit(X_train[['radius_worst', 'perimeter_worst']], y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictions_all = rfc_best.predict(X_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictions_main_features = rfc_main_features.predict(X_test[['radius_worst', 'perimeter_worst']])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 9. Оценка модели с описанием выбранной метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Распределение данных по целевому признаку неравномерно (37%), поэтому в качестве метрики будем использовать precision"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "round(precision_score(y_test, predictions_all), 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для выборки со всеми признаками мы получили достаточно высокую точность (около 96%-99% в зависимости от данных)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "round(precision_score(y_test, predictions_main_features), 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучение на всего лишь двух признаках (radius_worst и perimeter_worst) выдает метрику precision около 91%-93%"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Похоже, что для высокого качества классификации опухолей достаточно иметь представление хотя бы об их размере"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть 10. Выводы "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "RandomForestClassifier обученный на тренировочной выборке выдает 96%-99% по метрике precision.\n",
    "\n",
    "Наибольшее значение для классификации имеют только 2-3 признака, причиной этому может быть либо природа данных, либо их искусственность.\n",
    "Возможно, выборка слишком узкая, а в реальной жизни доброкачественная и злокачественная опухоли могут иметь одинаковый размер, но отличаться по другим признакам.\n",
    "\n",
    "Также не ясно, как наша модель поведет себя на большом количестве данных, т.к. у нас в выборке меньше 600 сэмплов. Скорее всего модель потребуется переобучить, вследсвие чего метрика может снизиться.\n",
    "\n",
    "Однако, по графику с валидационной кривой видно, что по мере увеличения выборки растет и точность.\n",
    "\n",
    "Вывод: для использования в реальных условиях необходимо обеспечить модель необходимым количеством информации, после чего можно будет понять целесообразно ее использовать или нет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
